"""
感知机，基于单层神经网络
"""
import numpy as np
import random
from singlelayerneuralnetwork import SingleLayerNeuralNetwork

class Perceptron(SingleLayerNeuralNetwork):
    """
    感知机
    使用 model = Perceptron(iteration=10000, randomstate=0) 定义示例
    使用 model.fit(Xtrain, Ytrain, learningrate=0.5, fastmode=False, stochastic=False, minerror=0.0) 训练样本
    使用 model.predict(Xtest) 进行测试
    """

    def __sign(self, inputdata):
        '''
        二值函数
        inputdata：二值函数输入变量
        返回值：   二值函数输出变量
        '''
        if inputdata > 0:
            return 1
        elif inputdata < 0:
            return -1
        else:
            return 0


    def fit(self, Xtrain, Ytrain, learningrate=0.5, fastmode=False, stochastic=False, minerror=0.0, checkloss=False):
        '''
        训练测试样本
        Xtrain：      训练样本的数据，N * d 矩阵，第 i 行表示第 i 个数据
        Ytrain：      训练样本的标签，N * 1 矩阵，第 i 行表示第 i 个标签
        learningrate：学习率
        fastmode：    快速模式，可以加快训练速度，但是也会增加过拟合的风险
        stochastic：  随机模式
        minerror：    可容许的最小误差
        checkloss：   打印每次更新后的损失值
        @return：     (实际迭代的次数, 训练样本集上测试的准确率)
        '''
        N, D = Xtrain.shape  # N：训练样本的个数；D：训练样本的维数

        # 修改训练样本以使用算法需要
        Xtrain_ = np.append(np.ones((N, 1), dtype=Xtrain.dtype), Xtrain, 1)
        
        # 获取迭代起始样本的序号
        random.seed(self.randomstate)
        start_index = int(random.random() * N)

        # 初始化分类面W和损失值loss
        W = Xtrain_[start_index, :]
        W = W.reshape(D + 1, 1)
        loss = 1.0
        
        # 用来迭代估计参数的容器
        new_W = W.copy()
        new_loss = loss

        # 分类错误的数据样本及其标签，用来更新 W 和 w_0
        mis_x, mis_y = (Xtrain_[start_index - 1, :].reshape(1, D + 1), Ytrain[start_index - 1, 0])
        
        # 开始迭代，最大迭代次数为 iteration
        for cycle in range(self.iteration):
            # 使用分类错误样本更新 W 和 w_0
            while True:
                new_W = new_W + learningrate * mis_y * mis_x.reshape(D + 1, 1)
                if fastmode == True and self.__sign(np.matmul(mis_x, new_W)[0,0]) != mis_y:
                    continue
                else:
                    break
            
            # 累计更新后的估计参数向量分类正确的样本数量
            new_loss = 0
            
            for i in range(- start_index, N - start_index):
                x = Xtrain_[i, :].reshape(1, D + 1)  # 第 i 个数据
                y = Ytrain[i, 0]  # 第 i 个数据对应的标签
                
                # 使用估计参数 W 和 w_0 对数据 x 进行预测，得到结果 y_predict
                y_predict = self.__sign(np.matmul(x, new_W)[0,0])
                if y_predict == y:
                    new_loss += 1
                else:
                    # 记录分类错误的样本数据及其标签
                    mis_x = x.copy()
                    mis_y = y
                    if stochastic == False:
                        break

            new_loss = 1 - new_loss / N

            # 打印更新后的损失值
            if checkloss:
                print(new_loss)

            # 获得更优的估计参数向量
            if stochastic == False or new_loss < loss:
                W = new_W.copy()
                loss = new_loss

            # 得到最优估计参数向量
            if loss <= minerror:
                break
        
        # 记录结果
        self.W = W[1:,:].reshape(D)
        self.w_0 = W[0,0]
        self.dimen = D
        self.Xtrain = Xtrain
        self.Ytrain = Ytrain
        self.actual_iteration = cycle + 1
        self.train_score = loss
        
        return (self.actual_iteration, self.train_score)


    def predict(self, Xtest):
        '''
        对测试样本进行预测
        Xtest：  测试样本的数据，N * d 矩阵，第 i 行表示第 i 个数据
        @return：测试样本的预测结果，N * 1 矩阵，第 i 行表示第 i 个标签
        '''
        # 调用父类的predict方法检查初始条件是否成立
        super().predict(Xtest)

        # N：测试样本的数量；D：测试样本的维数
        N, D = Xtest.shape
        W = np.transpose(self.W.reshape(1, D))
        w_0 = self.w_0

        # 对测试集进行预测
        y_predict = np.zeros((N, 1))
        for i in range(N):
            x = Xtest[i, :].reshape(1, D)  # 测试集第 i 个数据
            y_predict[i, 0] = self.__sign(np.matmul(x, W)[0,0] + w_0)
        
        # 保存测试数据集和测试结果集
        self.Xtest = Xtest
        self.Ypred = y_predict
        
        return self.Ypred


if __name__ == "__main__":
    X_train = np.array(
        [[1,2],[5,7],[1,1],[8,5],[4,1],
         [3,1],[4,9],[3,10],[3,2],[2,1],
         [8,8],[7,9],[2,0],[0,0],[6,11],
         [2,9],[4,2],[3,9],[6,0],[6,9]]
    )
    Y_train = np.array(
        [[1],[-1],[1],[-1],[1],
         [1],[-1],[-1],[1],[1],
         [-1],[-1],[1],[1],[-1],
         [-1],[1],[-1],[1],[-1]]
    )
    X_test = np.array([[0.5,0.5],[4,8],[3,10],[5,6]])

    model = Perceptron()
    iteration, score = model.fit(X_train, Y_train)
    y_pred = model.predict(X_test)
    w_0, W = model.getW()

    print(("Iteration = {0}\n" + 
          "Score = {1}\n" + 
          "W = {2}\n" + 
          "w_0 = {3}")
          .format(iteration, score, W, w_0))
    print("y_pred = \n" + str(y_pred))
    model.drawTwoDimenPic(testdata=True)